package injections;

import com.google.common.base.Preconditions;
import injections.annotations.Property;
import injections.converters.Converter;
import injections.converters.atomic.AtomicBooleanConverter;
import injections.converters.atomic.AtomicIntegerConverter;
import injections.converters.atomic.AtomicLongConverter;
import injections.converters.common.FileConverter;
import injections.converters.common.StringConverter;
import injections.converters.primitive.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Created by rehak on 2/11/14.
 */
public class Injector {

    private static final Logger LOGGER = LoggerFactory.getLogger(Injector.class);
    private final ConcurrentMap<Class<?>, Converter> converters = new ConcurrentHashMap<>();
    private final DefinedInjection inj;
    private final SingletonHolder holder = SingletonHolder.createInstance();

    public Injector(DefinedInjection inj) {
        this.inj = inj;
        // add default converters
        addConverter(new ByteConverter());
        addConverter(new BooleanConverter());
        addConverter(new CharConverter());
        addConverter(new DoubleConverter());
        addConverter(new FloatConverter());
        addConverter(new IntConverter());
        addConverter(new LongConverter());
        addConverter(new ShortConverter());
        addConverter(new StringConverter());
        addConverter(new FileConverter());
        addConverter(new AtomicBooleanConverter());
        addConverter(new AtomicIntegerConverter());
        addConverter(new AtomicLongConverter());
    }

    /**
     * Add custom converter
     *
     * @param converter
     */
    public void addConverter(Converter converter) {
        Preconditions.checkNotNull(converter);
        Set<Class<?>> supported = converter.supportedClasses();
        for (Class<?> sup : supported) {
            if (converters.keySet().contains(sup)) {
                throw new IllegalArgumentException("Class " + sup + " already supported!");
            }
        }
        for (Class<?> sup : supported) {
            converters.put(sup, converter);
        }
    }

    /**
     * Inject object defined as root object
     *
     * @return
     */
    public Object injectRoot() throws Exception {
        return inject(inj.getRootInstanceName());
    }

    /**
     * Inject specifid object
     *
     * @param instanceName
     * @return
     */
    public Object inject(String instanceName) throws Exception {
        for (DefinedClass c : inj.getClasses()) {
            if (c.getInstanceName().equals(instanceName)) {
                return create(c);
            }
        }
        throw new IllegalArgumentException("Instance " + instanceName + " not found!");
    }

    /**
     * Create defined class
     *
     * @param c
     * @return
     * @throws IllegalArgumentException
     */
    private Object create(DefinedClass c) throws Exception {
        // instance name set, no class name- refers to instance defined somewhere else - find it and instantiate
        if (c.getClassName() == null && c.getInstanceName() != null) {
            return inject(c.getInstanceName());
        } else if (c.getClassName() == null && c.getInstanceName() == null) {
            // no instance name and no class name - anonymous convertor class
            throw new RuntimeException("Unable to create anonymous converter class!");
        } else if (c.getClassName() != null && c.getInstanceName() == null) {
            // no instance name and class name - anonymous inner class
            c.setInstanceName(UUID.randomUUID().toString());
        }
        // already instantiated? return from singleton holder
        Object inst = holder.getSingleton(c.getInstanceName());
        if (inst != null) {
            return inst;
        }
        // create instance
        Object obj;
        try {
            obj = newInstance(c);
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException("Class " + c.getClassName() + " can not be instantionated ", ex);
        }
        // injectFieldValue fields
        List<Field> injectable = InjectionAnalyzer.getFieldsWithAnnotation(obj.getClass(), Property.class);
        for (Field f : injectable) {
            injectField(f, obj, c);
        }
        Method[] postInjectMethods = InjectionAnalyzer.getPostInjectMethods(obj.getClass());
        for (Method m : postInjectMethods) {
            try {
                m.setAccessible(true);
                m.invoke(obj);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new IllegalArgumentException("Unable to invoke @PostInject method on " + obj.getClass() + " - instance " + c.getInstanceName(), e);
            }
        }
        holder.putSingleton(c.getInstanceName(), obj);
        return obj;
    }

    /**
     * Instantiate class with supplied constructor parameters or using parameterless constructor
     *
     * @param c DefinedClass to be instantiated
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws InvocationTargetException
     */
    private Object newInstance(DefinedClass c) throws Exception {
        // check class
        Class<?> aClass;
        // check if available
        try {
            aClass = Class.forName(c.getClassName());
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException("Class " + c.getClassName() + " not found!", ex);
        }
        // check class injectability
        if (!InjectionAnalyzer.isInjectable(aClass)) {
            throw new RuntimeException("Class " + aClass + " is not injectable (invalid constructor).");
        }
        if (c.getConstructorProps().isEmpty()) {
            // no constructor parameteres
            return aClass.newInstance();
        } else {
            // find constructor with annotations
            Constructor constructor = null;
            for (Constructor cc : aClass.getConstructors()) {
                if (InjectionAnalyzer.isAnnotatedConstructor(cc)) {
                    if (constructor != null) {
                        throw new RuntimeException("Multiple annotated constructors found! " + aClass);
                    }
                    constructor = cc;
                }
            }
            if (constructor == null) {
                throw new RuntimeException("No suitable annotated constructor found! " + aClass);
            }
            Class[] params = constructor.getParameterTypes();
            Type[] genericParameterTypes = constructor.getGenericParameterTypes();

            List<DefinedProperty> defProps = c.getConstructorProps();
            if (params.length != defProps.size()) {
                throw new RuntimeException("Constructor formal " + params.length + " and actual " + defProps.size() + " parameter size differs!");
            }
            Object[] args = new Object[params.length];
            for (int i = 0; i < params.length; i++) {
                Class<?> generic = Object.class;
                if (genericParameterTypes[i] != null) {
                    if (genericParameterTypes[i] instanceof ParameterizedType) {
                        ParameterizedType pt = (ParameterizedType) genericParameterTypes[i];
                        Type[] ta = pt.getActualTypeArguments();
                        if (ta[0] != null) {
                            Type gt = ta[0];
                            generic = (Class<?>) gt;
                        }
                    }
                }
                args[i] = createConstructorParameter(params[i], generic, defProps.get(i));
            }
            Object o = constructor.newInstance(args);
            return o;
        }
    }

    private Object createConstructorParameter(Class<?> parameterCls, Class<?> genericType, DefinedProperty dp) throws Exception {
        if (InjectionAnalyzer.isArray(parameterCls)) {
            Object toInject = createArrayToInject(parameterCls, dp);
            return toInject;
        } else if (InjectionAnalyzer.isCollection(parameterCls)) {
            Collection col = createCollectionToInject(parameterCls, genericType, dp);
            return col;
        } else {
            Object toInject = createObjectToInject(parameterCls, dp);
            return toInject;
        }
    }

    /**
     * @param f
     * @param obj
     * @param c
     */
    private void injectField(Field f, Object obj, DefinedClass c) throws Exception {
        String propName = f.getName();
        DefinedProperty dp = c.getProperty(propName);
        if (dp == null) {
            if (!f.getAnnotation(Property.class).optional()) {
                // missing value
                throw new IllegalArgumentException("No suitable value for field " + propName + " in instance " + c.getInstanceName());
            } else {
                // not set
                return;
            }
        }
        Class<?> fieldCls = f.getType();
        if (InjectionAnalyzer.isArray(f)) {
            Object toInject = createArrayToInject(fieldCls, dp);
            injectFieldValue(f, obj, toInject);
        } else if (InjectionAnalyzer.isCollection(f)) {
            Collection col = createCollectionToInject(fieldCls, InjectionAnalyzer.getCollectionType(f), dp);
            f.setAccessible(true);
            if (f.get(obj) == null) {
                // null collection
                injectFieldValue(f, obj, col);
            } else {
                // add elements to existing
                Collection existing = (Collection) f.get(obj);
                for (Object o : col) {
                    existing.add(o);
                }
            }
        } else {
            Object toInject = createObjectToInject(fieldCls, dp);
            injectFieldValue(f, obj, toInject);
        }
    }

    private Object createObjectToInject(Class<?> fieldClass, DefinedProperty dp) throws Exception {
        if (dp.isEmpty()) {
            throw new RuntimeException("No value for " + dp.getName());
        }
        if (dp.isMulti()) {
            throw new RuntimeException("Multiple value for " + dp.getName());
        }
        if (dp.isSingle()) {
            Object val = createValue(dp.getValues().get(0), fieldClass);
            return val;
        }
        throw new RuntimeException("Unknown state for " + dp.getName());
    }

    /**
     * If single object value => provider for collection or null.
     * If provider and collection is null - assign provided collection, if provider and collection is not null, put elements from provider into existing collection.
     * If multiple values => collection elements
     *
     * @param dp
     */
    private Collection createCollectionToInject(Class<?> fieldClass, Class<?> collectionComponentType, DefinedProperty dp) throws Exception {
        if (dp.isEmpty()) {
            return new ArrayList<>();
        }
        // single = null or provider
        if (dp.isSingle()) {
            PropertyValue pv = dp.getValues().get(0);
            // create value
            Object val = createValue(pv, fieldClass);
            if (val == null) {
                return null;
            }
            List<Object> list = new ArrayList<>();
            list.add(val);
            return list;
        }
        if (dp.isMulti()) {
            List<Object> list = new ArrayList<>();
            for (int i = 0; i < dp.getValues().size(); i++) {
                PropertyValue propVal = dp.getValues().get(i);
                Object val = createValue(propVal, collectionComponentType);
                list.add(val);
            }
            return list;
        }
        throw new RuntimeException("Unknown state for " + dp.getName());
    }

    /**
     * If single object value => provider for array or null
     * If multiple values => array members
     *
     * @param dp
     */
    private Object createArrayToInject(Class<?> fieldClass, DefinedProperty dp) throws Exception {
        // if single, check null or provider
        if (dp.isSingle()) {
            PropertyValue pv = dp.getValues().get(0);
            // null ?
            if (pv.getVal() == null) {
                return null;
            }
            // provider
            if (pv.isReference()) {
                return createValue(pv, fieldClass);
            }
        }
        Class componentType = InjectionAnalyzer.getArrayMembersClass(fieldClass);
        Object ar = Array.newInstance(componentType, dp.getValues().size());
        for (int i = 0; i < dp.getValues().size(); i++) {
            PropertyValue propVal = dp.getValues().get(i);
            Object val = createValue(propVal, componentType);
            Array.set(ar, i, val);
        }
        return ar;
    }

    /**
     * Create object for single property value
     *
     * @param pv
     * @param fieldType
     * @return
     */
    private Object createValue(PropertyValue pv, Class<?> fieldType) throws Exception {
        Object created;
        // reference
        if (pv.isReference()) {
            DefinedClass dc = pv.getAsReference();
            if (dc.getClassName() == null && dc.getInstanceName() == null) {
                if (isConvertable(fieldType)) {
                    return convert(pv, fieldType);
                }
            }
            Object op = create(dc);
            // provider ?
            if (op instanceof Provider) {
                Provider p = (Provider) op;
                return p.getInstance();
            } else {
                // value
                return op;
            }
        } else if (pv.isValue()) {
            // null
            if (pv.getVal() == null) {
                return null;
            }
            // convertable
            if (isConvertable(fieldType)) {
                return convert(pv, fieldType);
            }
            // enum
            if (fieldType.isEnum()) {
                if (!(pv.getVal() instanceof String)) {
                    throw new RuntimeException("Expection String as val for enum, was " + pv.getVal().getClass());
                }
                String enumVal = (String) pv.getVal();
                Enum.valueOf((Class<? extends Enum>) fieldType, enumVal);
            }
        }
        throw new IllegalStateException("Invalid state reached!");
    }

    private Object convert(PropertyValue pv, Class<?> fieldType) {
        Converter conv = converters.get(fieldType);
        if (pv.isReference()) {
            return conv.convertValue(pv.getAsReference().getProperties(), fieldType);
        } else {
            DefinedProperty dp = new DefinedProperty(Converter.DEF_PROP_NAME, Arrays.asList(new PropertyValue[]{pv}));
            return conv.convertValue(Arrays.asList(new DefinedProperty[]{dp}), fieldType);
        }

    }

    private boolean isConvertable(Class<?> fieldType) {
        return converters.keySet().contains(fieldType);
    }


    private Object checkProvider(Object v) {
        if (v instanceof Provider) {
            try {
                return ((Provider) v).getInstance();
            } catch (Exception e) {
                throw new RuntimeException("Provider threw exception!", e);
            }
        }
        return v;
    }

    private void injectFieldValue(Field field, Object fieldObject, Object fieldValue) {
        field.setAccessible(true);
        try {
            field.set(fieldObject, fieldValue);
        } catch (Exception e) {
            throw new IllegalArgumentException("Unable to inject " + fieldValue + " to field " + field.getName(), e);
        }
    }


}
